package com.bitsfromspace.ubertorrent;

import java.io.File;
import java.io.InputStream;
import java.util.ArrayList;
import java.util.Arrays;
import java.util.List;
import java.util.Map;

import com.bitsfromspace.ubertorrent.bencoding.BDecoder;
import com.bitsfromspace.ubertorrent.bencoding.BValue;
import com.bitsfromspace.ubertorrent.bencoding.BValue.Type;
import com.bitsfromspace.ubertorrent.tracker.Tracker;
import com.bitsfromspace.ubertorrent.tracker.TrackerTier;
import com.bitsfromspace.ubertorrent.tracker.Trackers;

/**
 * @author: chris.oudeman
 * @since: Jan 5, 2011
 */
public class Torrent {

	private final Sha1Hash infoHash;
	private final Trackers trackers;
	private final TorrentFileInfo[] files;
	private final int pieceLength;
	private final Sha1Hash[] pieceHashes;
	private final String comment;


	public Torrent(
			Sha1Hash infoHash,
			Trackers trackers,
			TorrentFileInfo[] files,
			int pieceLength,
			Sha1Hash[] pieceHashes,
			String comment) {

		this.infoHash = infoHash;
		this.trackers = trackers;
		this.files = files;
		this.pieceLength = pieceLength;
		this.pieceHashes = pieceHashes;
		this.comment = comment;
	}

	/**
	 * Returns the <em>infoHash</em> for this torrent.
	 */
	public Sha1Hash getInfoHash() {

		return infoHash;
	}

	/**
	 * Returns the {@link Trackers} for this torrent.
	 */
	public Trackers getTrackers() {

		return trackers;
	}

	/**
	 * returns the comment for this torrent (optional)
	 */
	public String getComment() {

		return comment;
	}

	/**
	 * Returns an ordered array of {@link TorrentFileInfo} files.
	 */
	public TorrentFileInfo[] getFiles() {

		return files;
	}

	/**
	 * Returns the piece length of this torrent (How many bytes per piece).
	 */
	public int getPieceLength() {

		return pieceLength;
	}

	/**
	 * returns an ordered array of {@link Sha1Hash} piece hashes.
	 */
	public Sha1Hash[] getPieceHashes() {

		return pieceHashes;
	}

	/**
	 * Parses a torrent from a .torrent-file
	 */
	public static Torrent parseFrom(InputStream in) throws TorrentParseException {

		BValue torrentDataValue = BDecoder.decode(in).iterator().next();
		if (torrentDataValue == null || torrentDataValue.getType() != Type.MAP) {
			throw new TorrentParseException("Invalid torrent.");
		}
		Map<?, ?> torrentData = torrentDataValue.getMapUnwrapped();
		Map<?, ?> infoData = (Map<?, ?>) torrentData.get("info");
		
		Map<BValue, BValue> infoDataValue =
				torrentDataValue.getMap().get(new BValue("info")).getMap();

		BValue pieceData = infoDataValue.get(new BValue("pieces"));

		Trackers trackers = parseTrackers(torrentData);
		TorrentFileInfo[] files = parseFiles(infoData);
		int pieceLength = ((Long) infoData.get("piece length")).intValue();
		Sha1Hash[] pieceHashes = parsePieceHashes(pieceData);
		String comment = (String) torrentData.get("comment");//might be null

		Sha1Hash infoHash = Sha1Hash.calculate(new BValue(infoDataValue));

		return new Torrent(
				infoHash,
				trackers,
				files,
				pieceLength,
				pieceHashes,
				comment);
	}

	protected static Trackers parseTrackers(Map<?, ?> torrentData)
														throws TorrentParseException {

		try {

			List<TrackerTier> tiers = new ArrayList<TrackerTier>();
			if (torrentData.containsKey("announce-list")) {
				@SuppressWarnings("unchecked")
				List<List<String>> lolTrackers =
						(List<List<String>>) torrentData.get("announce-list");
				for (List<String> trackerUrls : lolTrackers) {
					List<Tracker> trackerTier = new ArrayList<Tracker>();
					for (String trackerUrl : trackerUrls) {
						trackerTier.add(new Tracker(trackerUrl));
					}
					tiers.add(new TrackerTier(trackerTier));
				}
				return new Trackers(tiers);
			} else { // fall back to announce 
				Tracker tracker =
						new Tracker((String) torrentData.get("announce"));
				return new Trackers(Arrays.asList(new TrackerTier(Arrays
						.asList(tracker))));
			}
		} catch (Exception ex) {
			throw new TorrentParseException(ex);
		}

	}

	@SuppressWarnings("unchecked")
	protected static TorrentFileInfo[] parseFiles(Map<?, ?>  torrentData)
															throws TorrentParseException {
		
		if (torrentData.containsKey("files")){

			List<Map<?, ?>> filesData = (List<Map<?, ?>>) torrentData.get("files");
			TorrentFileInfo[] files = new TorrentFileInfo[filesData.size()];
			for (int i = 0 ; i < filesData.size(); i ++){
				Map<?,?> fileData = filesData.get(i);
				files[i] = new TorrentFileInfo(
								concatFileElement((List<String>) fileData.get("path")),
						(Long) fileData.get("length"));
			}
			return files;
		} else {
			TorrentFileInfo[] files = new TorrentFileInfo[]{
					new TorrentFileInfo(
							(String) torrentData.get("name"),
							(Long) torrentData.get("length"))
			};
			return files;
		}
		

	}

	protected static String concatFileElement(List<String> elements) {

		StringBuilder buffer = new StringBuilder();
		for (int i = 0; i < elements.size(); i++) {
			buffer.append(elements.get(i));
			if (i < elements.size() - 1) {
				buffer.append(File.separator);
			}
		}
		return buffer.toString();
	}

	protected static Sha1Hash[] parsePieceHashes(BValue pieceData)

																throws TorrentParseException {
		
		byte[] allPieceHashes = pieceData.getByteString();

		Sha1Hash[] hashes = new Sha1Hash[allPieceHashes.length / 20];

		for (int i = 0; i < allPieceHashes.length; i += 20) {
			byte[] hash = new byte[20];
			System.arraycopy(allPieceHashes, i, hash, 0, 20);
			hashes[i / 20] = new Sha1Hash(hash);
		}
		return hashes;
	}

}


